//
// Copyright Contributors to the MaterialX Project
// SPDX-License-Identifier: Apache-2.0
//

#ifndef MATERIALX_INTERFACE_H
#define MATERIALX_INTERFACE_H

/// @file
/// Interface element subclasses

#include <MaterialXCore/Export.h>

#include <MaterialXCore/Geom.h>

MATERIALX_NAMESPACE_BEGIN

class PortElement;
class Input;
class Output;
class InterfaceElement;
class Node;
class NodeDef;

/// A shared pointer to a PortElement
using PortElementPtr = shared_ptr<PortElement>;
/// A shared pointer to a const PortElement
using ConstPortElementPtr = shared_ptr<const PortElement>;

/// A shared pointer to an Input
using InputPtr = shared_ptr<Input>;
/// A shared pointer to a const Input
using ConstInputPtr = shared_ptr<const Input>;

/// A shared pointer to an Output
using OutputPtr = shared_ptr<Output>;
/// A shared pointer to a const Output
using ConstOutputPtr = shared_ptr<const Output>;

/// A shared pointer to an InterfaceElement
using InterfaceElementPtr = shared_ptr<InterfaceElement>;
/// A shared pointer to a const InterfaceElement
using ConstInterfaceElementPtr = shared_ptr<const InterfaceElement>;

using CharSet = std::set<char>;

/// @class PortElement
/// The base class for port elements such as Input and Output.
///
/// Port elements support spatially-varying upstream connections to nodes.
class MX_CORE_API PortElement : public ValueElement
{
  protected:
    PortElement(ElementPtr parent, const string& category, const string& name) :
        ValueElement(parent, category, name)
    {
    }

  public:
    virtual ~PortElement() { }

  protected:
    using NodePtr = shared_ptr<Node>;
    using ConstNodePtr = shared_ptr<const Node>;

  public:
    /// @name Node Name
    /// @{

    /// Set the node name string of this element, creating a connection to
    /// the Node with the given name within the same NodeGraph.
    void setNodeName(const string& node)
    {
        setAttribute(NODE_NAME_ATTRIBUTE, node);
    }

    /// Return true if this element has a node name string.
    bool hasNodeName() const
    {
        return hasAttribute(NODE_NAME_ATTRIBUTE);
    }

    /// Return the node name string of this element.
    const string& getNodeName() const
    {
        return getAttribute(NODE_NAME_ATTRIBUTE);
    }

    /// @}
    /// @name Node Graph
    /// @{

    /// Set the node graph string of this element.
    void setNodeGraphString(const string& node)
    {
        setAttribute(NODE_GRAPH_ATTRIBUTE, node);
    }

    /// Return true if this element has a node graph string.
    bool hasNodeGraphString() const
    {
        return hasAttribute(NODE_GRAPH_ATTRIBUTE);
    }

    /// Return the node graph string of this element.
    const string& getNodeGraphString() const
    {
        return getAttribute(NODE_GRAPH_ATTRIBUTE);
    }

    /// @}
    /// @name Output
    /// @{

    /// Set the output string of this element.
    void setOutputString(const string& output)
    {
        setAttribute(OUTPUT_ATTRIBUTE, output);
    }

    /// Return true if this element has an output string.
    bool hasOutputString() const
    {
        return hasAttribute(OUTPUT_ATTRIBUTE);
    }

    /// Set the output to which this input is connected.  If the output
    /// argument is null, then any existing output connection will be cleared.
    void setConnectedOutput(ConstOutputPtr output);

    /// Return the output, if any, to which this input is connected.
    OutputPtr getConnectedOutput() const;

    /// Return the output string of this element.
    const string& getOutputString() const
    {
        return getAttribute(OUTPUT_ATTRIBUTE);
    }

    /// @}
    /// @name Connections
    /// @{

    /// Set the node to which this element is connected.  The given node must
    /// belong to the same node graph.  If the node argument is null, then
    /// any existing node connection will be cleared.
    void setConnectedNode(ConstNodePtr node);

    /// Return the node, if any, to which this element is connected.
    virtual NodePtr getConnectedNode() const;

    /// @}
    /// @name Validation
    /// @{

    /// Validate that the given element tree, including all descendants, is
    /// consistent with the MaterialX specification.
    bool validate(string* message = nullptr) const override;

    /// @}

  public:
    static const string NODE_NAME_ATTRIBUTE;
    static const string NODE_GRAPH_ATTRIBUTE;
    static const string OUTPUT_ATTRIBUTE;
};

/// @class Input
/// An input element within a Node or NodeDef.
///
/// An Input holds either a uniform value or a connection to a spatially-varying
/// Output, either of which may be modified within the scope of a Material.
class MX_CORE_API Input : public PortElement
{
  public:
    Input(ElementPtr parent, const string& name) :
        PortElement(parent, CATEGORY, name)
    {
    }
    virtual ~Input() { }

  public:
    /// @name Default Geometric Property
    /// @{

    /// Set the defaultgeomprop string for the input.
    void setDefaultGeomPropString(const string& geomprop)
    {
        setAttribute(DEFAULT_GEOM_PROP_ATTRIBUTE, geomprop);
    }

    /// Return true if the given input has a defaultgeomprop string.
    bool hasDefaultGeomPropString() const
    {
        return hasAttribute(DEFAULT_GEOM_PROP_ATTRIBUTE);
    }

    /// Return the defaultgeomprop string for the input.
    const string& getDefaultGeomPropString() const
    {
        return getAttribute(DEFAULT_GEOM_PROP_ATTRIBUTE);
    }

    /// Return the GeomPropDef element to use, if defined for this input.
    GeomPropDefPtr getDefaultGeomProp() const;

    /// @}
    /// @name Connections
    /// @{

    /// Return the node, if any, to which this input is connected.
    NodePtr getConnectedNode() const override;

    /// Connects this input to a corresponding interface with the given name.
    /// If the interface name specified is an empty string then any existing connection is removed.
    void setConnectedInterfaceName(const string& interfaceName);

    /// Return the input on the parent graph corresponding to the interface name
    /// for this input.
    InputPtr getInterfaceInput() const;

    /// @}
    /// @name Hints
    /// @{

    /// Return true if the input has a hint
    bool hasHint() const
    {
        return hasAttribute(HINT_ATTRIBUTE);
    }

    /// Return the code generation hint
    const string& getHint() const
    {
        return getAttribute(HINT_ATTRIBUTE);
    }

    // Set the code generation hint
    void setHint(const string& hint)
    {
        setAttribute(HINT_ATTRIBUTE, hint);
    }

    /// @}
    /// @name Validation
    /// @{

    /// Validate that the given element tree, including all descendants, is
    /// consistent with the MaterialX specification.
    bool validate(string* message = nullptr) const override;

    /// @}

  public:
    static const string CATEGORY;
    static const string DEFAULT_GEOM_PROP_ATTRIBUTE;
    static const string HINT_ATTRIBUTE;
    static const string TRANSPARENCY_HINT;
    static const string OPACITY_HINT;
    static const string ANISOTROPY_HINT;
};

/// @class Output
/// A spatially-varying output element within a NodeGraph or NodeDef.
class MX_CORE_API Output : public PortElement
{
  public:
    Output(ElementPtr parent, const string& name) :
        PortElement(parent, CATEGORY, name)
    {
    }
    virtual ~Output() { }

  public:
    /// @name Traversal
    /// @{

    /// Return the Edge with the given index that lies directly upstream from
    /// this element in the dataflow graph.
    Edge getUpstreamEdge(size_t index = 0) const override;

    /// Return the number of queryable upstream edges for this element.
    size_t getUpstreamEdgeCount() const override
    {
        return 1;
    }

    /// Return true if a cycle exists in any upstream path from this element.
    bool hasUpstreamCycle() const;

    /// @}
    /// @name Validation
    /// @{

    /// Validate that the given element tree, including all descendants, is
    /// consistent with the MaterialX specification.
    bool validate(string* message = nullptr) const override;

    /// @}

  public:
    static const string CATEGORY;
    static const string DEFAULT_INPUT_ATTRIBUTE;
};

/// @class InterfaceElement
/// The base class for interface elements such as Node, NodeDef, and NodeGraph.
///
/// An InterfaceElement supports a set of Input and Output elements, with an API
/// for setting their values.
class MX_CORE_API InterfaceElement : public TypedElement
{
  protected:
    InterfaceElement(ElementPtr parent, const string& category, const string& name) :
        TypedElement(parent, category, name),
        _inputCount(0),
        _outputCount(0)
    {
    }

  public:
    virtual ~InterfaceElement() { }

  protected:
    using NodeDefPtr = shared_ptr<NodeDef>;
    using ConstNodeDefPtr = shared_ptr<const NodeDef>;

  public:
    /// @name NodeDef String
    /// @{

    /// Set the NodeDef string for the interface.
    void setNodeDefString(const string& nodeDef)
    {
        setAttribute(NODE_DEF_ATTRIBUTE, nodeDef);
    }

    /// Return true if the given interface has a NodeDef string.
    bool hasNodeDefString() const
    {
        return hasAttribute(NODE_DEF_ATTRIBUTE);
    }

    /// Return the NodeDef string for the interface.
    const string& getNodeDefString() const
    {
        return getAttribute(NODE_DEF_ATTRIBUTE);
    }

    /// @}
    /// @name Inputs
    /// @{

    /// Add an Input to this interface.
    /// @param name The name of the new Input.
    ///     If no name is specified, then a unique name will automatically be
    ///     generated.
    /// @param type An optional type string.
    /// @return A shared pointer to the new Input.
    InputPtr addInput(const string& name = EMPTY_STRING,
                      const string& type = DEFAULT_TYPE_STRING)
    {
        InputPtr child = addChild<Input>(name);
        child->setType(type);
        return child;
    }

    /// Return the Input, if any, with the given name.
    InputPtr getInput(const string& name) const
    {
        return getChildOfType<Input>(name);
    }

    /// Return a vector of all Input elements.
    vector<InputPtr> getInputs() const
    {
        return getChildrenOfType<Input>();
    }

    /// Return the number of Input elements.
    size_t getInputCount() const
    {
        return _inputCount;
    }

    /// Remove the Input, if any, with the given name.
    void removeInput(const string& name)
    {
        removeChildOfType<Input>(name);
    }

    /// Return the first Input with the given name that belongs to this
    /// interface, taking interface inheritance into account.
    InputPtr getActiveInput(const string& name) const;

    /// Return a vector of all Input elements that belong to this interface,
    /// taking inheritance into account.
    vector<InputPtr> getActiveInputs() const;

    /// @}
    /// @name Outputs
    /// @{

    /// Add an Output to this interface.
    /// @param name The name of the new Output.
    ///     If no name is specified, then a unique name will automatically be
    ///     generated.
    /// @param type An optional type string.
    /// @return A shared pointer to the new Output.
    OutputPtr addOutput(const string& name = EMPTY_STRING,
                        const string& type = DEFAULT_TYPE_STRING)
    {
        OutputPtr output = addChild<Output>(name);
        output->setType(type);
        return output;
    }

    /// Return the Output, if any, with the given name.
    OutputPtr getOutput(const string& name) const
    {
        return getChildOfType<Output>(name);
    }

    /// Return a vector of all Output elements.
    vector<OutputPtr> getOutputs() const
    {
        return getChildrenOfType<Output>();
    }

    /// Return the number of Output elements.
    size_t getOutputCount() const
    {
        return _outputCount;
    }

    /// Remove the Output, if any, with the given name.
    void removeOutput(const string& name)
    {
        removeChildOfType<Output>(name);
    }

    /// Return the first Output with the given name that belongs to this
    /// interface, taking interface inheritance into account.
    OutputPtr getActiveOutput(const string& name) const;

    /// Return a vector of all Output elements that belong to this interface,
    /// taking inheritance into account.
    vector<OutputPtr> getActiveOutputs() const;

    /// Set the output to which the given input is connected, creating a
    /// child input if needed.  If the node argument is null, then any
    /// existing output connection on the input will be cleared.
    void setConnectedOutput(const string& inputName, OutputPtr output);

    /// Return the output connected to the given input.  If the given input is
    /// not present, then an empty OutputPtr is returned.
    OutputPtr getConnectedOutput(const string& inputName) const;

    /// @}
    /// @name Tokens
    /// @{

    /// Add a Token to this interface.
    /// @param name The name of the new Token.
    ///     If no name is specified, then a unique name will automatically be
    ///     generated.
    /// @return A shared pointer to the new Token.
    TokenPtr addToken(const string& name = EMPTY_STRING)
    {
        return addChild<Token>(name);
    }

    /// Return the Token, if any, with the given name.
    TokenPtr getToken(const string& name) const
    {
        return getChildOfType<Token>(name);
    }

    /// Return a vector of all Token elements.
    vector<TokenPtr> getTokens() const
    {
        return getChildrenOfType<Token>();
    }

    /// Remove the Token, if any, with the given name.
    void removeToken(const string& name)
    {
        removeChildOfType<Token>(name);
    }

    /// Return the first Token with the given name that belongs to this
    /// interface, taking interface inheritance into account.
    TokenPtr getActiveToken(const string& name) const;

    /// Return a vector of all Token elements that belong to this interface,
    /// taking inheritance into account.
    vector<TokenPtr> getActiveTokens() const;

    /// @}
    /// @name Value Elements
    /// @{

    /// Return the ValueElement, if any, with the given name.
    ValueElementPtr getValueElement(const string& name) const
    {
        return getChildOfType<ValueElement>(name);
    }

    /// Return the first value element with the given name that belongs to this
    /// interface, taking interface inheritance into account.
    /// Examples of value elements are Input, Output, and Token.
    ValueElementPtr getActiveValueElement(const string& name) const;

    /// Return a vector of all value elements that belong to this interface,
    /// taking inheritance into account.
    /// Examples of value elements are Input, Output, and Token.
    vector<ValueElementPtr> getActiveValueElements() const;

    /// @}
    /// @name Values
    /// @{

    /// Set the typed value of an input by its name, creating a child element
    /// to hold the input if needed.
    template <class T> InputPtr setInputValue(const string& name,
                                              const T& value,
                                              const string& type = EMPTY_STRING);

    /// Return the typed value of an input by its name, taking both the calling
    /// element and its declaration into account.
    /// @param name The name of the input to be evaluated.
    /// @param target An optional target name, which will be used to filter
    ///    the declarations that are considered.
    /// @return If the given input is found in this interface or its
    ///    declaration, then a shared pointer to its value is returned;
    ///    otherwise, an empty shared pointer is returned.
    ValuePtr getInputValue(const string& name, const string& target = EMPTY_STRING) const;

    /// Set the string value of a Token by its name, creating a child element
    /// to hold the Token if needed.
    TokenPtr setTokenValue(const string& name, const string& value)
    {
        TokenPtr token = getToken(name);
        if (!token)
            token = addToken(name);
        token->setValue<string>(value);
        return token;
    }

    /// Return the string value of a Token by its name, or an empty string if
    /// the given Token is not present.
    string getTokenValue(const string& name)
    {
        TokenPtr token = getToken(name);
        return token ? token->getValueString() : EMPTY_STRING;
    }

    /// @}
    /// @name Target
    /// @{

    /// Set the target string of this interface.
    void setTarget(const string& target)
    {
        setAttribute(TARGET_ATTRIBUTE, target);
    }

    /// Return true if the given interface has a target string.
    bool hasTarget() const
    {
        return hasAttribute(TARGET_ATTRIBUTE);
    }

    /// Return the target string of this interface.
    const string& getTarget() const
    {
        return getAttribute(TARGET_ATTRIBUTE);
    }

    /// @}
    /// @name Version
    /// @{

    /// Set the version string of this interface.
    void setVersionString(const string& version)
    {
        setAttribute(VERSION_ATTRIBUTE, version);
    }

    /// Return true if this interface has a version string.
    bool hasVersionString() const
    {
        return hasAttribute(VERSION_ATTRIBUTE);
    }

    /// Return the version string of this interface.
    const string& getVersionString() const
    {
        return getAttribute(VERSION_ATTRIBUTE);
    }

    /// Set the major and minor versions as an integer pair.
    void setVersionIntegers(int majorVersion, int minorVersion);

    /// Return the major and minor versions as an integer pair.
    virtual std::pair<int, int> getVersionIntegers() const;

    /// @}
    /// @name Default Version
    /// @{

    /// Set the default version flag of this element.
    void setDefaultVersion(bool defaultVersion)
    {
        setTypedAttribute<bool>(DEFAULT_VERSION_ATTRIBUTE, defaultVersion);
    }

    /// Return the default version flag of this element.
    bool getDefaultVersion() const
    {
        return getTypedAttribute<bool>(DEFAULT_VERSION_ATTRIBUTE);
    }

    /// @}
    /// @name Utility
    /// @{

    /// Return the first declaration of this interface, optionally filtered
    ///    by the given target name.
    /// @param target An optional target name, which will be used to filter
    ///    the declarations that are considered.
    /// @return A shared pointer to declaration, or an empty shared pointer if
    ///    no declaration was found.
    virtual ConstInterfaceElementPtr getDeclaration(const string& target = EMPTY_STRING) const;

    /// Clear all attributes and descendants from this element.
    void clearContent() override;

    /// Return true if this instance has an exact input match with the given
    /// declaration, where each input of this the instance corresponds to a
    /// declaration input of the same name and type.
    ///
    /// If an exact input match is not found, and the optional message argument
    /// is provided, then an error message will be appended to the given string.
    bool hasExactInputMatch(ConstInterfaceElementPtr declaration, string* message = nullptr) const;

    /// @}

  public:
    static const string NODE_DEF_ATTRIBUTE;
    static const string TARGET_ATTRIBUTE;
    static const string VERSION_ATTRIBUTE;
    static const string DEFAULT_VERSION_ATTRIBUTE;

  protected:
    void registerChildElement(ElementPtr child) override;
    void unregisterChildElement(ElementPtr child) override;

  private:
    size_t _inputCount;
    size_t _outputCount;
};

template <class T> InputPtr InterfaceElement::setInputValue(const string& name,
                                                            const T& value,
                                                            const string& type)
{
    InputPtr input = getChildOfType<Input>(name);
    if (!input)
        input = addInput(name);
    input->setValue(value, type);
    return input;
}

MATERIALX_NAMESPACE_END

#endif
